using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;
using System.Threading;
using System.Xml;
using System.Reflection;

namespace CodePlex.Resourceful
{

    public class Client
    {
        public static readonly string ProjectUri = "http://resourceful.codeplex.com";
        private static readonly string DefaultUserAgent;
        public static bool UseSystemWebProxy = false;

        static Client() 
        {

#if SILVERLIGHT
            Client.DefaultUserAgent = string.Format("{0} {1} ({2})",
                typeof(Client).FullName,
                "0.0.2 Silverlight",
                ProjectUri); 
#else
            ServicePointManager.Expect100Continue = true;      // otherwise: An unexpected error occurred on a receive.
            

            Client.DefaultUserAgent = string.Format("{0} {1} ({2})",
                typeof(Client).FullName,
                typeof(Client).Assembly.GetName().Version.ToString(3),
                ProjectUri);

            // TODO this should be configurable
            // also per service-point?
            ServicePointManager.DefaultConnectionLimit = int.MaxValue;

#endif
        }


        
        private Uri _rootUri;
        private readonly Tracer _tracer;
        private string _userAgent;
        private TimeSpan _throttle;
        private DateTime _lastRequestTime;
        private NameValuePairs _commonHeaders = new NameValuePairs();


        public Client(string rootUri)
            :this(new Uri(rootUri))
        {

        }
        public Client(Uri rootUri)
        {
            _userAgent = DefaultUserAgent;
            this.RootUri = rootUri;

            _tracer = new Tracer();
        }


        public Uri RootUri
        {
            get
            {
                return _rootUri;
            }
            set
            {
                _rootUri = value;
            }
        }

        public NameValuePairs CommonHeaders
        {
            get
            {
                return _commonHeaders;
            }
        }


        public Tracer Tracer
        {
            get
            {
                return _tracer;
            }
        }


        public string UserAgent
        {
            get
            {
                return _userAgent;
            }
            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    value = DefaultUserAgent;
                }
                _userAgent = value;
            }
        }

        public TimeSpan Throttle
        {
            get
            {
                return _throttle;
            }
            set
            {
                _throttle = value;
            }
        }




#if SILVERLIGHT
        private static IEnumerable<string> All(WebHeaderCollection headers)
        {
            return headers.Headers;
        }
#else

        private readonly CredentialCache _credentials = new CredentialCache();  // does not exist on SL

        private static IEnumerable<string> All(WebHeaderCollection headers)
        {
            return headers.AllKeys;
        }

        
        public void SetBasicAuth(string path, string name, string password)
        {
            Uri prefix = new Uri(this.RootUri + path);
            string type = "Basic";

            _credentials.Remove(prefix, type);  // just in case
            _credentials.Add(prefix, type, new NetworkCredential(name, password));
        }
#endif





        // get

        public ClientResponse GetRepresentation(GetRequest request)
        {
            return this.DoRequest(request);
        }

        public ClientResponse GetRepresentation(string path)
        {
            return this.GetRepresentation(GetRequest.Create(path));
        }

        public ClientResponse GetRepresentation(string path, NameValuePairs parameters)
        {
            return this.GetRepresentation(GetRequest.Create(path, parameters));
        }

        public ClientResponse GetRepresentation(string path, string mediaType, NameValuePairs parameters)
        {
            return this.GetRepresentation(GetRequest.Create(path,mediaType,parameters));
        }

        public ClientResponse GetRepresentation(string path, string mediaType)
        {
            return this.GetRepresentation(GetRequest.Create(path, mediaType));
        }


        public ClientResponse GetRepresentation(string path, string mediaType, DateTime ifModifiedSince)
        {
            return this.GetRepresentation(GetRequest.Create(path, mediaType,ifModifiedSince));
        }



        // head

        public ClientResponse HeadRepresentation(HeadRequest request)
        {
            return this.DoRequest(request);
        }


        // post

        public ClientResponse PostRepresentation(string path, string mediaType, string representation)
        {
            return this.PostRepresentation(PostRequest.Create(path, mediaType, representation));
        }
        public ClientResponse PostRepresentation(string path, string mediaType, Stream representation)
        {
            return this.PostRepresentation(PostRequest.Create(path, mediaType, representation));
        }
        public ClientResponse PostRepresentation(string path, string mediaType, NameValuePairs parameters)
        {
            return this.PostRepresentation(PostRequest.Create(path, mediaType, parameters));
        }

        public ClientResponse PostRepresentation(PostRequest request)
        {
            return this.DoRequest(request);
        }


        // put 

        public ClientResponse PutRepresentation(string path, string mediaType, string representation)
        {
            return this.PutRepresentation(PutRequest.Create(path, mediaType, representation));
        }

        public ClientResponse PutRepresentation(string path, string mediaType, Stream representation)
        {
            return this.PutRepresentation(PutRequest.Create(path, mediaType, representation));
        }

        public ClientResponse PutRepresentation(string path, string mediaType, NameValuePairs representation)
        {
            return this.PutRepresentation(PutRequest.Create(path, mediaType, representation));
        }

        public ClientResponse PutRepresentation(PutRequest request)
        {
            return this.DoRequest(request);
        }


        // delete


        public ClientResponse DeleteRepresentation(DeleteRequest request)
        {
            return this.DoRequest(request);
        }
        public ClientResponse DeleteRepresentation(string path)
        {
            return this.DeleteRepresentation(DeleteRequest.Create(path));
        }







      

































        private void TraceResponseHeaders(HttpWebResponse response)
        {
            _tracer.WriteLine();
            _tracer.WriteLine("{0} {1}", (int)response.StatusCode, response.StatusDescription);

#if !SILVERLIGHT
            foreach (string header in All(response.Headers))
            {
                _tracer.WriteLine("{0}:{1}", header, response.Headers[header]);
            }
#endif
        }


        private void TraceRequestHeaders(HttpWebRequest request, bool setContentLength)
        {

            _tracer.WriteLine();

            string contentLength = null;
#if SILVERLIGHT
            _tracer.WriteLine("{0} {1}", request.Method, Utils.GetPathAndQuery(request.RequestUri));
            contentLength = request.Headers[HttpHeader.ContentLength];
#else
            _tracer.WriteLine("{0} {1} HTTP/{2}", request.Method, Utils.GetPathAndQuery(request.RequestUri), request.ProtocolVersion.ToString(2));
            contentLength = request.ContentLength.ToString();
#endif

            _tracer.WriteLine("{0}:{1}", HttpHeader.Host, request.RequestUri.Host);


            if (setContentLength)
            {
                _tracer.WriteLine("{0}:{1}", HttpHeader.ContentLength, contentLength);
            }

            foreach (string header in All(request.Headers))
            {
                _tracer.WriteLine("{0}:{1}", header, request.Headers[header]);
            }
            
        }






        protected void DoRequest(ClientRequest request, Action<ClientResponse> callback)
        {
            this.InternalDoRequest(request, callback);
        }

        protected ClientResponse DoRequest(ClientRequest request)
        {
            return this.InternalDoRequest(request, null) as ClientResponse;
        }


        private object InternalDoRequest(ClientRequest request, Action<ClientResponse> callback)
        {
            string path = request.Path;

            if (path.StartsWith(this.RootUri.ToString()))
            {
                path = path.Substring(this.RootUri.ToString().Length);
            }

            while (this.Throttle != TimeSpan.Zero && DateTime.Now < _lastRequestTime + this.Throttle)
            {
                Thread.Sleep(10);
            }


            NameValuePairs headers = request.Headers ?? new NameValuePairs();

#if SILVERLIGHT

            // double escape!!!!
            if (!EnvironmentInfo.IsMac)
            {
                path = path.Replace("%", "%25");    
            }
            HttpWebRequest httpRequest = HttpWebRequest.Create(new Uri(this.RootUri.ToString() + path)) as HttpWebRequest;
            // request.Headers[HttpHeader.UserAgent] = this.UserAgent;   // COM exception

#else

            Uri requestUri = new Uri(this.RootUri +  path);
            

            HttpWebRequest httpRequest = HttpWebRequest.Create(requestUri) as HttpWebRequest;

            // don't buffer the write stream if content-length is provided
            if (headers.ContainsKey(HttpHeader.ContentLength))
            {
                httpRequest.AllowWriteStreamBuffering = false;
            }


            // TODO these two should probably be configurable at some point
            httpRequest.ReadWriteTimeout = Timeout.Infinite;
            httpRequest.Timeout = Timeout.Infinite;

            // proxy support
            if (UseSystemWebProxy)
                httpRequest.Proxy = WebRequest.GetSystemWebProxy();
            if (httpRequest.Proxy != null)
                httpRequest.Proxy.Credentials = CredentialCache.DefaultCredentials;


            httpRequest.Credentials = _credentials;
            httpRequest.AllowAutoRedirect = false;
            httpRequest.UserAgent = this.UserAgent;
#endif


            httpRequest.Method = request.Method;

            foreach (string header in this.CommonHeaders.Keys)
            {
                headers[header] = this.CommonHeaders[header];
            }

          
            // set request headers
            string hostHeader = null;
            bool setContentLength = false;
            foreach (string header in headers.Keys)
            {
                string value = headers[header];

                if (Utils.ContainsUnprintableUsAsciiChars(value))
                {
                    throw CodePlexException.Format("Header '{0}' value '{1}' contains one or more unprintable US-ASCII characters, which is unsupported.", header, value);
                }

                try
                {
                    if (header == HttpHeader.ContentType)
                    {
                        httpRequest.ContentType = value;
                    }

#if !SILVERLIGHT
                    else if (header == HttpHeader.Accept)
                    {
                        httpRequest.Accept = value;
                    }
                    else if (header == HttpHeader.IfModifiedSince)
                    {
                        httpRequest.IfModifiedSince = Utils.FromHttpDate(value);
                    }
                    else if (header == HttpHeader.ContentLength)
                    {
                        httpRequest.ContentLength = long.Parse(value);
                        setContentLength = true;
                    }
                    else if (header == HttpHeader.UserAgent)
                    {
                        httpRequest.UserAgent = value;
                    }
                    else if (header == HttpHeader.Date)
                    {
                        httpRequest.Headers.GetType().InvokeMember(
                            "AddWithoutValidate",
                            BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
                            null,
                            httpRequest.Headers,
                            new object[] { header, value });
                    }
#endif
                    else if (header == HttpHeader.Host)
                    {
                        hostHeader = value;
                    }

                    else
                    {
                        httpRequest.Headers[header] = value;
                    }

                }
                catch (Exception ex)
                {
                    throw CodePlexException.Format(ex, "Invalid header '{0}' value '{1}' ({2})", header, value,ex.Message);
                }
            }

            // apply hacks to the request, if necessary
            UriHacker uriHacker = new UriHacker(httpRequest, hostHeader);



            // trace the request headers
            bool hadListeners = _tracer.HasListeners;
            if (hadListeners)
            {
                _tracer.WriteLine();
                this.TraceRequestHeaders(httpRequest, setContentLength);
            }


            // write to the request stream
            if (request.InputStream != null && request.Method != HttpMethod.Head)
            {
#if SILVERLIGHT
                AsyncCallback finish = delegate(IAsyncResult result) 
                {
                    Stream requestStream = httpRequest.EndGetRequestStream(result);
                    this.WriteRequest(request, hadListeners, requestStream);
                    this.StartResponse(request, callback, httpRequest, uriHacker, hadListeners);
                };
                httpRequest.BeginGetRequestStream(finish, null);
                return null;
#else
                Stream requestStream = requestStream = httpRequest.GetRequestStream();
                this.WriteRequest(request,hadListeners, requestStream);
#endif

            }
            return this.StartResponse(request,callback,httpRequest,uriHacker,hadListeners);

        }


        private void WriteRequest(ClientRequest request, bool hadListeners, Stream requestStream)
        {
            if (hadListeners)
            {
                requestStream = new MonitoredStream(requestStream, _tracer.Write);
            }

            using (Stream inputStream = request.InputStream)
            {
                if (EnvironmentInfo.IsSilverlight)
                {
                    Utils.BufferedTransfer(inputStream, requestStream, request.BytesIn);
                    requestStream.Flush();
                }
                else
                {
                    using (requestStream)
                    {
                        Utils.BufferedTransfer(inputStream, requestStream, request.BytesIn);
                    }
                }
            }
        }

        private ClientResponse StartResponse(ClientRequest request, Action<ClientResponse> callback, HttpWebRequest httpRequest,
            UriHacker uriHacker, bool hadListeners)
        {

            _lastRequestTime = DateTime.Now;


            if (callback != null)
            {
                AsyncCallback finish = delegate(IAsyncResult rt)
                {

                    HttpWebResponse response = null;
                    WebException webex = null;
                    try
                    {
                        using (uriHacker)
                        {
                            response = httpRequest.EndGetResponse(rt) as HttpWebResponse;
                        }
                    }
                    catch (WebException ex)
                    {
                        webex = ex;
                        response = ex.Response as HttpWebResponse;  // response buried in the exception
                    }

                    ClientResponse response2 = GetResponse(response, webex, hadListeners, request.BufferOutput, request.BytesOut);

                    callback(response2);

                };

                httpRequest.BeginGetResponse(finish, null);


                return null;
            }
            else
            {
#if SILVERLIGHT
                Fn<bool> checkUI = delegate
                {
                    try
                    {
                        // .Window will throw if not on ui
                        return System.Windows.Browser.HtmlPage.Window != null;
                    }
                    catch (InvalidOperationException)
                    {
                        return false;
                    }

                };
                if (checkUI())
                {
                    throw new InvalidOperationException("Synchronous call will deadlock if called from the UI thread.");
                }
                ManualResetEvent signal = new ManualResetEvent(false);
                ClientResponse rt = null;
                Action<ClientResponse> callback2 = delegate(ClientResponse response) 
                {
                    rt = response;
                    signal.Set();
                };
                this.StartResponse(request, callback2, httpRequest, uriHacker, hadListeners);
                signal.WaitOne();
                return rt;
#else

                // send the request, get the response
                HttpWebResponse response = null;
                WebException webex = null;
                try
                {
                    using (uriHacker)
                    {
                        response = httpRequest.GetResponse() as HttpWebResponse;
                    }
                }
                catch (WebException ex)
                {
                    webex = ex;
                    response = ex.Response as HttpWebResponse;  // response buried in the exception
                }


                return GetResponse(response, webex, hadListeners, request.BufferOutput, request.BytesOut);
#endif
            }

        }

  


        private ClientResponse GetResponse( HttpWebResponse response, WebException webex, bool hadListeners, bool bufferOutput, Action<long> bytesOut)
        {
            if (response == null)
            {
                throw webex;
            }
          

            // trace the response headers
            if (hadListeners)
            {
                this.TraceResponseHeaders(response);
            }

            // read status and headers
            HttpStatus status = HttpStatus.FromCode(response.StatusCode);
            NameValuePairs responseHeaders =
#if SILVERLIGHT
                new NameValuePairs();

            if (!string.IsNullOrEmpty(response.ContentType))
            {
                responseHeaders[HttpHeader.ContentType] = response.ContentType;
            }
            responseHeaders[HttpHeader.ContentLength] = response.ContentLength.ToString();

#else
                NameValuePairs.From(response.Headers);
#endif

            if (EnvironmentInfo.IsMono && status.Equals(HttpStatus.NoContent))
            {
                // don't even try to get the response stream (mono won't give you one...)
                return new ClientResponse(status, responseHeaders, Utils.ToStream(string.Empty));
            }


            // handle the response stream
            Stream responseStream = null;

            if (hadListeners)
            {
                responseStream = new MonitoredStream(response.GetResponseStream(), _tracer.Write);
            }
            else
            {
                responseStream = response.GetResponseStream();
            }


            // read the response
            if (bufferOutput)
            {

                // buffer: read entire stream up front (and close the response stream)
                MemoryStream ms = new MemoryStream();

                if (responseStream != null)
                {
                    Utils.BufferedTransfer(responseStream, ms, bytesOut);
                    ms.Position = 0;
                    response.Close();
                }

                return new ClientResponse(status, responseHeaders, ms);
            }
            else
            {
                // don't buffer: the responsbility of closing the response stream is on the caller
                return new ClientResponse(status, responseHeaders, responseStream);
            }
        }


        public void GetRepresentation(GetRequest request, Action<ClientResponse> callback)
        {
            this.DoRequest(request, callback);
        }

        public void PostRepresentation(PostRequest request, Action<ClientResponse> callback)
        {
            this.DoRequest(request, callback);
        }

    }
}
